<?php
class X_mvc extends X{
    protected $bootstrapClass= "X_bootstrap";
    protected $router;
    public $request;
    public $bootstrap;
    
    public function __construct($indexFile="index.php",$defaultController="index",$defaultAction="index",$defaultRoute="/:controller/:action/"){
        X_options::$mvc= true;
        $this->defaultController= $defaultController;
        $this->defaultAction= $defaultAction;
        $this->router= new X_router(new X_route("default",$defaultRoute));
        $this->request= new X_request($indexFile,$defaultController,$defaultAction,$this->router);
        X_connectDatabase($this->request->database);
        $this->request->mysql= & $this->request->database;
        if(X_config::$mvc_i18n_database == "mysql"){
            X_connectI18N($this->request->i18n, $this->request->database, $this->request->lang);
        }
        
    }
    
    public function setBootstrapClass($bootstrap){
        $this->bootstrapClass= $bootstrap;
        return $this;
    }
    
    public function run(){
        $this->bootstrap= new $this->bootstrapClass($this->request);
        return $this;
    }
    
    public function addRoute($name,$route="",$controller="index",$action="index",$params=array()){
        $this->router->addRoute($name,$route,$controller,$action,$params);
    }
    
}

class X_route extends X{
    public $name;
    public $route;
    public $controller;
    public $action;
    public $params;
    public function __construct($name="default",$route="/:controller/:action/",$controller="index",$action="index",$params=array()) {
        $this->name= $name;
        $this->route= $route;
        $this->controller= $controller;
        $this->action= $action;
        $this->params= $params;
    }
}

class X_router extends X{
    public $routes= array();
    public function __construct($defaultRoute) {
        $this->routes[]= $defaultRoute;
    }
    public function addRoute($name,$route="",$controller="index",$action="index",$params=array()){
        array_unshift($this->routes, new X_route($name,$route,$controller,$action,$params));
    }
    public function getRoute($route){
        foreach($this->routes as $r){
            if($r->name==$route)return $r;
        }
        return new X_route();
    }
    public function matchRoutes($checkRoute, $requestRoute){
        if(count($checkRoute)==count($requestRoute)){
            foreach($checkRoute as $key=>$param){
                if(substr($param,0,1)!=":" && $param !== $requestRoute[$key] && $param !== "") return false;
            }
            return true;
        }
        return false;
    }
    public function normalizeRoute($route){
        if(substr($route,0,1)!='/')$route= "/".$route;
        if(substr($route,-1,1)!='/')$route= $route."/";
        return $route;
    }
    
}

class X_request extends X{
    protected $request;
    protected $controller;
    protected $action;
    protected $params;
    protected $defaultController;
    protected $defaultAction;
    protected $defaultControllerSuffix= "Controller";
    protected $defaultActionSuffix= "Action";
    protected $indexFile;
    public $router;
    public $currentRouteName;
    public $database;
    public $mysql;
    public $i18n;
    public $lang;
    public $regex_404OnUrl= "#(\.((png)|(ico)|(jpg)|(jpeg)|(php)|(html)|(phtml)|(phtml))$)#i";
    
    public function __construct($indexFile,$defaultController,$defaultAction,$router) {
        $this->router= $router;
        $this->indexFile= $indexFile;
        $this->defaultController= $defaultController;
        $this->defaultAction= $defaultAction;
    }
    
    public function decode(){
        $request= preg_replace("#^/".preg_quote($this->indexFile,"#")."#i","/",preg_replace("#^".preg_quote(preg_replace("#^(.*)/".preg_quote($this->indexFile,"#").".*$#i","$1",$_SERVER['PHP_SELF']),"#")."#i","",$_SERVER['REQUEST_URI']));
        if(strpos($request, "?"))$request= substr($request,0,strpos($request, "?"));
        if($request=="")$request= "/";
        if($this->regex_404OnUrl && preg_match($this->regex_404OnUrl, $request))header("HTTP/1.1 404 Not Found")&die('<h1>404 Not Found</h1>');
        $this->request= $request;
        $requestRoute= explode("/",$this->router->normalizeRoute($request));
        foreach($this->router->routes as $route){
            $routeName= $route->name;
            $checkRoute= explode("/",$this->router->normalizeRoute($route->route));
            if($this->router->matchRoutes($checkRoute,$requestRoute)){
                $this->currentRouteName= $routeName;
                $this->controller= (in_array(":controller", $checkRoute, true))? $requestRoute[array_search(":controller", $checkRoute, true)] : $route->controller;
                $this->action= (in_array(":action", $checkRoute, true))? $requestRoute[array_search(":action", $checkRoute, true)] : $route->action;
                $this->params= $route->params;
                foreach($checkRoute as $key=>$param){
                    if($param!="" && $param!=":controller" && $param!=":action" && substr($param,0,1)==':' && $param!=":"){
                        if(!array_key_exists(substr($param,1),$this->params) || $requestRoute[$key]!="")$this->params[substr($param,1)]= $requestRoute[$key];
                    }
                }
                break;
            }
            
        }
        if(!$this->controller){
            $request= explode("/", $request);
            $this->controller= (!empty($request[1]))? $request[1] : $this->defaultController;
            $this->action= (!empty($request[2]))? $request[2] : $this->defaultAction;
        }
        $this->lang= (isset($this->params['lang']))? $this->params['lang'] : ((isset($_GET['lang']))? $_GET['lang'] : ((isset($_SESSION['lang']))? $_SESSION['lang'] : null));
        if(!empty($this->lang)) $_SESSION['X_lang']= $this->lang;
        if(isset(X_config::$mvc_routes_i18n[$this->lang])){
            if(($controllerName= array_search($this->controller, X_config::$mvc_routes_i18n[$this->lang])))$this->controller= $controllerName;
            if(($ActionName= array_search($this->action, X_config::$mvc_routes_i18n[$this->lang])))$this->action= $ActionName;
        }
        return $this;
    }
    
    public function encodeUrl($controller=null,$action=null,$params=array(),$route=null){
        $doc_root= HTTP_ROOT;
        if(substr($doc_root,-1,1)=="/") $doc_root= substr($doc_root,0,-1);
        return $doc_root.$this->encode($controller, $action, $params, $route);
    }
    
    public function encode($controller=null,$action=null,$params=array(),$route=null){
        if($route===null)$route= $this->currentRouteName;
        $route= $this->router->getRoute($route);
        if($controller===null)$controller= $route->controller;
        if($action===null)$action= $route->action;
        $routeStr= $this->router->normalizeRoute($route->route);
        if(!isset($params['lang']) && !empty($this->lang))$params['lang']= $this->lang;
        foreach($params as $param=>$value){
            $routeStr= str_replace(":".$param,$value,$routeStr);
        }
        if(array_key_exists($controller, X_config::$mvc_routes_i18n[$params['lang']]))$controller= X_config::$mvc_routes_i18n[$params['lang']][$controller];
        if(array_key_exists($action, X_config::$mvc_routes_i18n[$params['lang']]))$action= X_config::$mvc_routes_i18n[$params['lang']][$action];
        return str_replace(array(":controller",":action"), array($controller,$action), $routeStr);
    }
    
    public function setDefaultController($defaultController){
        $this->defaultController= $defaultController;
        $this->decode();
        return $this;
    }
    
    public function setDefaultAction($defaultAction){
        $this->defaultAction= $defaultAction;
        $this->decode();
        return $this;
    }
    
    public function setDefaultSuffix($defaultControllerSuffix,$defaultActionSuffix){
        $this->defaultControllerSuffix= $defaultControllerSuffix;
        $this->defaultActionSuffix= $defaultActionSuffix;
        return $this;
    }
    
    public function setIndexFile($indexFile){
        $this->indexFile= $indexFile;
        $this->decode();
        return $this;
    }
    
    public function getIndexFile(){
        return $this->indexFile;
    }
    
    public function getRequest(){
        return $this->request;
    }
    public function getParams(){
        return $this->params;
    }
    
    public function getParam($param){
        return (isset($this->params[$param]))? $this->params[$param] : null;
    }
    public function getController(){
        return $this->controller;
    }
    public function getControllerName(){
        return $this->controller.$this->defaultControllerSuffix;
    }
    public function getAction(){
        return $this->action;
    }
    public function getActionName(){
        return $this->action.$this->defaultActionSuffix;
    }
    public function setRequest($request){
        $this->request= $request;
        $this->decode($this->indexFile);
        return $this;
    }
    public function setParams($params){
        $this->params= $params;
        return $this;
    }
    public function setController($controller){
        $this->controller= $controller;
        return $this;
    }
    public function setAction($action){
        $this->action= $action;
        return $this;
    }
    
    public function isPost(&$var,$post){
        if(isset($_POST[$post])){
            $var= $_POST[$post];
            return true;
        }
        return false;
    }
    public function setPost(&$var,$post){
        if(!empty($_POST[$post])){
            $var= $_POST[$post];
            return true;
        }
        return false;
    }
    
}

class X_bootstrap extends X{
    public $controller;
    public $request;
    
    public function __construct($request){
        $this->request= $request;
        $this->preDecode($this->request);
        $this->request->decode();
        $controller= $this->request->getControllerName();
        $action= $this->request->getActionName();
        $this->preController($this->request);
        $this->controller= new $controller($this->request);
        ob_start();
            $this->preDispatch($this->controller);
            $this->controller->preDispatch();
            foreach(X_options::$mvcPlugins as $plugin) $plugin->preDispatch($this->request,$this->controller);
            $this->controller->dispatch();
            foreach(X_options::$mvcPlugins as $plugin) $plugin->dispatch($this->request,$this->controller);
            $this->controller->init();
            foreach(X_options::$mvcPlugins as $plugin) $plugin->init($this->request,$this->controller);
            if(method_exists($this->controller, $action))$this->controller->$action();
            $this->preView($this->controller, $this->controller->view);
        X_ob::addOutput($this->request->getControllerName(),ob_get_clean());
        $this->controller->view();
    }
    
    public function preDecode($request){
        
    }
    
    public function preController($request){
        
    }
    
    public function preDispatch($controller){
        
    }
    
    public function preView($controller,$view){
        
    }
}

abstract class X_controller extends X{
    protected $viewClass;
    
    public $request;
    public $view;
    
    protected $layout= "default";
    
    final function __construct($request) {
        $this->request= $request;
        $this->viewClass= X_config::$mvc_defaultViewClass;
    }
    
    public function init(){
        
    }
    
    final public function dispatch(){
        $this->view= new $this->viewClass($this->request);
    }
    
    public function preDispatch(){
        
    }
    
    final public function setViewClass($viewClass){
        $this->viewClass= $viewClass;
        return $this;
    }
    
    final public function setLayout($layout){
        $this->layout= $layout;
        return $this;
    }
    
    public function getLayout(){
        return $this->layout;
    }
    
    public function view(){
        $this->view->init();
        $this->view->renderLayout($this->layout);
        return $this;
    }
    
    public function isPost(&$var,$post){
        if(isset($_POST[$post])){
            $var= $_POST[$post];
            return true;
        }
        return false;
    }
    public function setPost(&$var,$post){
        if(!empty($_POST[$post])){
            $var= $_POST[$post];
            return true;
        }
        return false;
    }
    
}

abstract class X_model extends X{
    protected $table;
    protected $engine= "mysql";
    protected $connectionID=null;
    protected $model;
    
    final public function __construct(){
        $link= null;
        switch($this->engine){
            case "mysql" : default : 
            if($this->connectionID!==null){
                $link= X_options::$mysql_connections[$this->connectionID];
            }
            $this->model= new X_mysql_query($this->table,$link);
            break;
            
        }
    }
    
    final public function fetchRowId($id, $field='id',$where=null,$cond=null,$escapeVal=null,$escapedVal2=null){
        return $this->model->fetchRowId($id, $field,$where,$cond,$escapeVal,$escapedVal2);
    }
    
    final public function lastInsertId(){
        return $this->model->lastInsertId();
    }
    
    final protected function select($select="*", $defaultTable=null){
        return $this->model->select($select, $defaultTable);
    }
    
    final protected function count($field="*"){
        return $this->model->count($field);
    }
    
    final protected function from($table){
        return $this->model->from($table);
    }
    
    final protected function update($values){
        return $this->model->update($values);
    }
    
    final protected function insert($values){
        return $this->model->insert($values);
    }
    
    final protected function delete(){
        return $this->model->delete();
    }
    
    final protected function where($cond, $escapedVal=null){
        return $this->model->where($cond, $escapedVal);
    }
    
    final protected function orWhere($cond, $escapedVal=null){
        return $this->model->orWhere($cond, $escapedVal);
    }
    
    final protected function join($table, $cond=null, $select=null, $type=""){
        return $this->model->join($table, $cond, $select, $type);
    }
    final protected function joinInner($table, $cond=null, $select=null){
        return $this->model->joinInner($table, $cond, $select, "INNER");
    }
    final protected function joinLeft($table, $cond=null, $select=null){
        return $this->model->joinLeft($table, $cond, $select, "LEFT");
    }
    final protected function joinRight($table, $cond=null, $select=null){
        return $this->model->joinRight($table, $cond, $select, "RIGHT");
    }
    final protected function innerJoin($table, $cond=null, $select=null){
        return $this->model->innerJoin($table, $cond, $select, "INNER");
    }
    final protected function leftJoin($table, $cond=null, $select=null){
        return $this->model->leftJoin($table, $cond, $select, "LEFT");
    }
    final protected function rightJoin($table, $cond=null, $select=null){
        return $this->model->rightJoin($table, $cond, $select, "RIGHT");
    }
    
    final protected function ordre($order){
        return $this->model->order($order);
    }
    
    final protected function limit($start, $length=null){
        return $this->model->limit($start, $length);
    }
    
    final protected function fetch($field){
        return $this->model->fetch($field);
    }
    
    final protected function fetchAll($associative=false){
        return $this->model->fetchAll($associative);
    }
    
    final protected function fetchRow(){
        return $this->model->fetchRow();
    }
    
    final protected function execute(){
        $return= $this->model->execute();
        $this->reset();
        return $return;
    }
    
    public function reset(){
        return $this->model->reset(true,true);
    }
    
}

class X_view extends X{
    public $request;
    protected $variables= array();
    
    final public function __set($name,$value){
        $this->variables[$name]= $value;
        return $value;
    }
    
    final public function __get($name){
        if(isset($this->variables[$name])) return $this->variables[$name];
        return null;
    }
    
    final public function __construct($request) {
        $this->request= $request;
    }
    
    public function init(){
        
    }
    
    public function appendCss($file=null,$inline=false){
        if($file===null){
            $file= "style.css";
        }
        if($inline){
            $filepath= DOCUMENT_ROOT."/css/".$file;
            if(is_file($filepath)){
                echo "\n<style type=\"text/css\">";
                    include $filepath;
                echo "\n</style>\n";
            }
        }else{
            $filepath= HTTP_ROOT."/css/".$file;
            echo "<link href=\"".$filepath."\" type=\"text/css\" rel=\"stylesheet\" />\n";
        }
    }
    
    public function appendScript($script){
        echo '<script type="text/javascript"><!--'."\n";
        echo $script;
        echo "\n// -->\n</script>\n";
    }
    
    public function appendJs($file=null,$inline=false){
        if($file===null){
            $file= "script.js";
        }
        if($inline){
            $filepath= DOCUMENT_ROOT."/js/".$file;
            if(is_file($filepath)){
                echo "\n<script type=\"text/javascript\"><!--\n";
                    include $filepath;
                echo "\n//-->\n</script>\n";
            }
        }else{
            $filepath= HTTP_ROOT."/js/".$file;
            echo "<script src=\"".$filepath."\" type=\"text/javascript\"></script>\n";
        }
    }
    
    protected function uri($url){
        return $url;
    }
    
    public function url($url=array(),$route=null){
        $doc_root= HTTP_ROOT;
        if(is_array($url)){
            if($route===null)$route=$this->request->currentRouteName;
            if(array_key_exists('controller',$url)){
                $controller= $url['controller'];
                unset($url['controller']);
            }else $controller= $this->request->getController();
            if(array_key_exists('action',$url)){
                $action= $url['action'];
                unset($url['action']);
            }else $action= $this->request->getAction();
            $params= $url;
            return $this->uri($this->request->encodeUrl($controller,$action,$params,$route));
        }else{
            if(substr($url,0,1)=="/") $url= substr($url,1);
            if(substr($doc_root,-1,1)=="/") $doc_root= substr($doc_root,0,-1);
            return $doc_root."/".$url;
        }
    }
    
    public function renderLayout($layout){
        if($layout)
            include APPLICATION_PATH."/layouts/".$layout.".phtml";
        else
            $this->render();
        return $this;
    }
    
    public function render($file= null){
        ob_start();
        $filepath= str_replace("//","/",(($file===null)? APPLICATION_PATH."/views/".$this->request->getController()."/".$this->request->getAction().".phtml" : $file));
        $fileArray = array(
            $filepath,
            $filepath.".php",
            $filepath.".phtml",
            APPLICATION_PATH."/views/".$filepath,
            APPLICATION_PATH."/views/".$filepath.".php",
            APPLICATION_PATH."/views/".$filepath.".phtml",
            APPLICATION_PATH."/views/".$this->request->getController()."/".$filepath,
            APPLICATION_PATH."/views/".$this->request->getController()."/".$filepath.".php",
            APPLICATION_PATH."/views/".$this->request->getController()."/".$filepath.".phtml",
            APPLICATION_PATH."/views/".$this->request->getController()."/".$this->request->getAction()."/".$filepath,
            APPLICATION_PATH."/views/".$this->request->getController()."/".$this->request->getAction()."/".$filepath.".php",
            APPLICATION_PATH."/views/".$this->request->getController()."/".$this->request->getAction()."/".$filepath.".phtml",
        );
        while(!is_file(str_replace("//","/",current($fileArray)))){
            if(next($fileArray)===false){
                //TODO: Custom Exception
                return;
            }
        }
        include str_replace("//","/",current($fileArray));
        return ob_get_clean();
    }
    
}

abstract class X_mvcPlugin extends X_plugin{
    
    public function preDispatch($request,$controller){
        
    }
    
    public function dispatch($request,$controller){
        
    }
    
    public function init($request,$controller){
        
    }
    
}
function X_registerMvcPlugin($plugin){
    X_options::$mvcPlugins[]= new $plugin();
}